/**
 * @file JSONNode.h
 * @brief Defines the JSONNode class, a fluent proxy for accessing and manipulating nlohmann::json data.
 *
 * @details This header provides the `JSONNode` class, which acts as a wrapper or proxy around
 * a reference to a `nlohmann::json` object. It offers a convenient, type-safe, and
 * intuitive Application Programming Interface (API) for navigating, querying, and modifying
 * JSON data structures.
 *
 * Key Features:
 * - **Fluent Interface:** Supports chainable access to nested elements using `operator[]` for both
 *   object keys (strings) and array indices (size_t).
 * - **Type Safety:** Provides methods like `isString()`, `isInt()`, etc., for type checking, and
 *   `asString()`, `asInt()`, etc., which return `std::optional<T>` to safely handle type
 *   mismatches or missing values. Includes `getXxxOrDefault()` helpers for convenience.
 * - **Read/Write Access:** Distinguishes between const and non-const contexts. Non-const
 *   `JSONNode` instances allow modification of the underlying JSON data via assignment
 *   (`operator=`).
 * - **Modification Notification:** When modifications occur through a non-const `JSONNode`,
 *   a provided callback (`ModificationNotifier`) is triggered, typically informing the
 *   owning `JSONHandler` instance that the data has changed.
 * - **Transparent Proxy:** Acts as a view; does not own the underlying JSON data. Its lifetime
 *   is independent of the data it references.
 * - **Automatic Creation (Non-const):** When accessing object keys that don't exist via
 *   the non-const `operator[]`, the key is automatically created with a `null` value,
 *   facilitating easy construction of JSON structures. If the node was `null`, it's
 *   promoted to an empty object first.
 *
 * Usage Example:
 * ```cpp
 * JSONHandler handler;
 * handler.loadFromJsonFile("config.json");
 *
 * // Read nested value safely
 * std::optional<int> timeoutOpt = handler.nodeAccessor()["network"]["timeout"].asInt();
 * if (timeoutOpt) {
 *     std::cout << "Timeout: " << timeoutOpt.value() << std::endl;
 * }
 *
 * // Read with default value
 * std::string logLevel = handler.nodeAccessor()["logging"]["level"].getStringOrDefault("INFO");
 *
 * // Modify a value (requires non-const accessor)
 * handler.nodeAccessor()["user"]["preferences"]["theme"] = "dark";
 *
 * // Access array element
 * handler.nodeAccessor()["users"][0]["enabled"] = true;
 *
 * // Save changes
 * if (handler.isDirty()) {
 *     handler.saveToFile("config_updated.json");
 * }
 * ```
 *
 * @note This class relies heavily on the `nlohmann::json` library for the underlying
 *       JSON implementation. Ensure `nlohmann/json.hpp` is correctly included and configured.
 * @note Linter errors regarding `<algorithm>` dependency are likely due to IntelliSense
 *       configuration issues (`includePath`) and not a direct code error in this file.
 *
 * @author liuzhiyu (liuzhiyu27@foxmail.com) // Assuming author based on JSONHandler.h
 * @copyright Copyright (c) 2023 by liuzhiyu, All Rights Reserved. // Assuming copyright
 * @version 1.0 // Example version
 * @date 2024-07-31 // Example date
 */

#ifndef JSON_NODE_H
#define JSON_NODE_H
#include <string>
#include <vector>
#include <memory>
#include <optional>
#include <functional> // Added for std::function
#include <iostream> // Added for std::cerr
#include <cstdint>  // Added for int64_t, uint64_t
#include <limits>   // Added for std::numeric_limits
#include "json.hpp"
#include <type_traits> // For SFINAE on assignment operator

// Forward declare JSONHandler to resolve dependency for ModificationNotifier type
class JSONHandler; 

/**
 * @class JSONNode
 * @brief A proxy class for accessing, navigating, and modifying JSON data with fluent interface.
 * 
 * JSONNode provides a convenient, type-safe, and intuitive API for accessing and navigating
 * JSON data. It acts as a proxy or view over a nlohmann::json object, allowing users to
 * query types, get values, navigate through nested structures, and modify values using
 * assignment operators when accessed through a non-const context.
 */
class __attribute__((visibility("default"))) JSONNode {
public:
    // Use ModificationNotifier type alias defined in JSONHandler
    // Need full definition of JSONHandler visible if using its type alias directly,
    // or redefine it here, or use std::function<void()> directly.
    // Using std::function<void()> directly is simplest to avoid header include issues.
    using ModificationNotifier = std::function<void()>; 

    /**
     * @brief Constructs a JSONNode that wraps a nlohmann::json object.
     * @param json Reference to the JSON object to wrap.
     * @param notifier A callback function to call when a modification occurs via this node.
     *                 Pass an empty function (`{}`) for read-only context.
     */
    JSONNode(nlohmann::json& json, ModificationNotifier notifier) 
        : m_jsonRef(json), m_notifier(std::move(notifier)) {}

    /**
     * @brief Checks if the node represents a JSON object.
     * @return True if the node is an object, false otherwise.
     */
    bool isObject() const { return m_jsonRef.is_object(); }

    /**
     * @brief Checks if the node represents a JSON array.
     * @return True if the node is an array, false otherwise.
     */
    bool isArray() const { return m_jsonRef.is_array(); }

    /**
     * @brief Checks if the node represents a string value.
     * @return True if the node is a string, false otherwise.
     */
    bool isString() const { return m_jsonRef.is_string(); }

    /**
     * @brief Checks if the node represents an integer value.
     * @return True if the node is an integer, false otherwise.
     */
    bool isInt() const { return m_jsonRef.is_number_integer(); }

    /**
     * @brief Checks if the node represents an unsigned 64-bit integer value.
     * @return True if the node is an unsigned 64-bit integer, false otherwise.
     */
    bool isUInt64() const { return m_jsonRef.is_number_unsigned(); }

    /**
     * @brief Checks if the node represents a floating-point value.
     * @return True if the node is a float, false otherwise.
     */
    bool isFloat() const { return m_jsonRef.is_number_float(); }

    /**
     * @brief Checks if the node represents a boolean value.
     * @return True if the node is a boolean, false otherwise.
     */
    bool isBool() const { return m_jsonRef.is_boolean(); }

    /**
     * @brief Checks if the node represents a null value.
     * @return True if the node is null, false otherwise.
     */
    bool isNull() const { return m_jsonRef.is_null(); }

    /**
     * @brief Gets a child node by key (const version).
     * @param key The key to look up.
     * @return A const-qualified JSONNode wrapping the child or null.
     */
    JSONNode get(const std::string& key) const {
        if (isObject() && m_jsonRef.contains(key)) {
            // Pass empty notifier for const context
            return JSONNode(const_cast<nlohmann::json&>(m_jsonRef.at(key)), {}); 
        }
        static nlohmann::json nullJson = nullptr;
        // Pass empty notifier for const context
        return JSONNode(nullJson, {});
    }

    /**
     * @brief Gets a child node from an array by index (const version).
     * @param index The index to look up.
     * @return A const-qualified JSONNode wrapping the child or null.
     */
    JSONNode at(size_t index) const {
        if (isArray() && index < m_jsonRef.size()) {
            // Pass empty notifier for const context
            return JSONNode(const_cast<nlohmann::json&>(m_jsonRef.at(index)), {});
        }
        static nlohmann::json nullJson = nullptr;
        // Pass empty notifier for const context
        return JSONNode(nullJson, {});
    }

    /**
     * @brief Convenience operator for accessing child nodes (const version).
     */
    JSONNode operator[](const std::string& key) const { return get(key); }

    /**
     * @brief Convenience operator for accessing array elements (const version).
     */
    JSONNode operator[](size_t index) const { return at(index); }
    
    /**
     * @brief Gets a child node by key (non-const version).
     * @param key The key to look up.
     * @return A mutable JSONNode wrapping the child (creating it if it doesn't exist in an object).
     */
    JSONNode get(const std::string& key) {
        if (isObject()) {
            // Pass the current notifier along
            return JSONNode(m_jsonRef[key], m_notifier); 
        } else if (isNull()) {
            m_jsonRef = nlohmann::json::object();
            if (m_notifier) m_notifier(); // Notify because we modified the null to an object
            // Pass the current notifier along
            return JSONNode(m_jsonRef[key], m_notifier); 
        }
        static nlohmann::json nullJson = nullptr;
        // Pass empty notifier for non-modifiable null node
        return JSONNode(nullJson, {}); 
    }

    /**
     * @brief Gets a child node from an array by index (non-const version).
     * @param index The index to look up.
     * @return A mutable JSONNode wrapping the child.
     */
    JSONNode at(size_t index) {
        if (isArray()) {
            try {
                 // Pass the current notifier along
                 return JSONNode(m_jsonRef.at(index), m_notifier);
            } catch (const nlohmann::json::out_of_range& e) {
                static nlohmann::json nullJson = nullptr;
                // Pass empty notifier for non-modifiable null node
                return JSONNode(nullJson, {});
            }
        }
         static nlohmann::json nullJson = nullptr;
         // Pass empty notifier for non-modifiable null node
         return JSONNode(nullJson, {}); 
    }

    /**
     * @brief Convenience operator for accessing child nodes (non-const version).
     * Creates the key with a null value if it doesn't exist and the current node is an object.
     */
    JSONNode operator[](const std::string& key) { return get(key); }

    /**
     * @brief Convenience operator for accessing array elements (non-const version).
     * Performs bounds checking.
     */
    JSONNode operator[](size_t index) { return at(index); }

    /**
     * @brief Checks if an object contains a specific key.
     * @param key The key to check for.
     * @return True if the key exists, false otherwise.
     */
    bool has(const std::string& key) const { return isObject() && m_jsonRef.contains(key); }

    /**
     * @brief Gets the size of an array or the number of keys in an object.
     * @return The size, or 0 if not an array or object.
     */
    size_t size() const {
        if (isArray() || isObject()) {
            return m_jsonRef.size();
        }
        return 0;
    }

    /**
     * @brief Gets the value as a string.
     * @return An optional string value, or empty if not a string.
     */
    std::optional<std::string> asString() const {
        if (isString()) {
            return m_jsonRef.get<std::string>();
        }
        return std::nullopt;
    }

    /**
     * @brief Gets the value as an int.
     * @return An optional int value if it's an integer and fits in int, or empty otherwise.
     */
    std::optional<int> asInt() const {
        if (isInt()) {
            try {
                long long val = m_jsonRef.get<long long>(); // Get as a wider type first
                if (val >= std::numeric_limits<int>::min() && val <= std::numeric_limits<int>::max()) {
                    return static_cast<int>(val);
                }
            } catch (const nlohmann::json::exception& e) {
                // Could log this error if needed
                // std::cerr << "JSONNode::asInt() error: " << e.what() << std::endl;
                return std::nullopt; // Error during get, or not a number
            }
        }
        return std::nullopt;
    }

    /**
     * @brief Gets the value as a 64-bit signed integer.
     * @return An optional int64_t value, or empty if not an integer.
     */
    std::optional<int64_t> asInt64() const {
        if (isInt()) { // is_number_integer() is appropriate here
            try {
                return m_jsonRef.get<int64_t>();
            } catch (const nlohmann::json::exception& e) {
                 // std::cerr << "JSONNode::asInt64() error: " << e.what() << std::endl;
                return std::nullopt;
            }
        }
        return std::nullopt;
    }

    /**
     * @brief Gets the value as a 64-bit unsigned integer.
     * @return An optional uint64_t value, or empty if not an unsigned integer.
     */
    std::optional<uint64_t> asUInt64() const {
        if (isUInt64()) { // is_number_unsigned() is appropriate here
            try {
                return m_jsonRef.get<uint64_t>();
            } catch (const nlohmann::json::exception& e) {
                // std::cerr << "JSONNode::asUInt64() error: " << e.what() << std::endl;
                return std::nullopt;
            }
        }
        return std::nullopt;
    }

    /**
     * @brief Gets the value as a floating-point value.
     * @return An optional floating-point value, or empty if not a floating-point.
     */
    std::optional<double> asDouble() const {
        if (m_jsonRef.is_number()) {
            return m_jsonRef.get<double>();
        }
        return std::nullopt;
    }

    /**
     * @brief Gets the value as a float.
     * @return An optional float value, or empty if not a float.
     */
    std::optional<float> asFloat() const {
        if (isFloat()) {
            return m_jsonRef.get<float>();
        }
        return std::nullopt;
    }

    /**
     * @brief Gets the value as a boolean.
     * @return An optional boolean value, or empty if not a boolean.
     */
    std::optional<bool> asBool() const {
        if (isBool()) {
            return m_jsonRef.get<bool>();
        }
        return std::nullopt;
    }

    /**
     * @brief Gets the string value at a given key.
     * @param key The key to look up.
     * @return An optional string value, or empty if the key doesn't exist or not a string.
     */
    std::optional<std::string> getString(const std::string& key) const { return get(key).asString(); }

    /**
     * @brief Gets the integer value (int) at a given key.
     * @param key The key to look up.
     * @return An optional int value, or empty if the key doesn't exist, not an integer, or out of int range.
     */
    std::optional<int> getInt(const std::string& key) const { return get(key).asInt(); }

    /**
     * @brief Gets the 64-bit signed integer value at a given key.
     * @param key The key to look up.
     * @return An optional int64_t value, or empty if the key doesn't exist or not an integer.
     */
    std::optional<int64_t> getInt64(const std::string& key) const { return get(key).asInt64(); }

    /**
     * @brief Gets the 64-bit unsigned integer value at a given key.
     * @param key The key to look up.
     * @return An optional uint64_t value, or empty if the key doesn't exist or not an unsigned integer.
     */
    std::optional<uint64_t> getUInt64(const std::string& key) const { return get(key).asUInt64(); }

    /**
     * @brief Gets the floating-point value at a given key.
     * @param key The key to look up.
     * @return An optional floating-point value, or empty if the key doesn't exist or not a floating-point.
     */
    std::optional<double> getDouble(const std::string& key) const { return get(key).asDouble(); }

    /**
     * @brief Gets the float value at a given key.
     * @param key The key to look up.
     * @return An optional float value, or empty if the key doesn't exist or not a float.
     */
    std::optional<float> getFloat(const std::string& key) const { return get(key).asFloat(); }

    /**
     * @brief Gets the boolean value at a given key.
     * @param key The key to look up.
     * @return An optional boolean value, or empty if the key doesn't exist or not a boolean.
     */
    std::optional<bool> getBool(const std::string& key) const { return get(key).asBool(); }

    /**
     * @brief Gets a string value with a default fallback.
     * @param key The key to look up.
     * @param defaultValue The value to return if the key doesn't exist or is not a string.
     * @return The string value or the default.
     */
    std::string getStringOrDefault(const std::string& key, const std::string& defaultValue) const {
        auto value = getString(key);
        return value.value_or(defaultValue);
    }

    /**
     * @brief Gets an integer value (int) with a default fallback.
     * @param key The key to look up.
     * @param defaultValue The value to return if the key doesn't exist, is not an integer, or out of int range.
     * @return The integer value or the default.
     */
    int getIntOrDefault(const std::string& key, int defaultValue) const {
        auto value = getInt(key);
        return value.value_or(defaultValue);
    }

    /**
     * @brief Gets a 64-bit signed integer value with a default fallback.
     * @param key The key to look up.
     * @param defaultValue The value to return if the key doesn't exist or is not an integer.
     * @return The 64-bit signed integer value or the default.
     */
    int64_t getInt64OrDefault(const std::string& key, int64_t defaultValue) const {
        auto value = getInt64(key);
        return value.value_or(defaultValue);
    }

    /**
     * @brief Gets a 64-bit unsigned integer value with a default fallback.
     * @param key The key to look up.
     * @param defaultValue The value to return if the key doesn't exist or is not an unsigned integer.
     * @return The 64-bit unsigned integer value or the default.
     */
    uint64_t getUInt64OrDefault(const std::string& key, uint64_t defaultValue) const {
        auto value = getUInt64(key);
        return value.value_or(defaultValue);
    }

    /**
     * @brief Gets a floating-point value with a default fallback.
     * @param key The key to look up.
     * @param defaultValue The value to return if the key doesn't exist or is not a floating-point.
     * @return The floating-point value or the default.
     */
    double getDoubleOrDefault(const std::string& key, double defaultValue) const {
        auto value = getDouble(key);
        return value.value_or(defaultValue);
    }

    /**
     * @brief Gets a float value with a default fallback.
     * @param key The key to look up.
     * @param defaultValue The value to return if the key doesn't exist or is not a float.
     * @return The float value or the default.
     */
    float getFloatOrDefault(const std::string& key, float defaultValue) const {
        auto value = getFloat(key);
        return value.value_or(defaultValue);
    }

    /**
     * @brief Gets a boolean value with a default fallback.
     * @param key The key to look up.
     * @param defaultValue The value to return if the key doesn't exist or is not a boolean.
     * @return The boolean value or the default.
     */
    bool getBoolOrDefault(const std::string& key, bool defaultValue) const {
        auto value = getBool(key);
        return value.value_or(defaultValue);
    }

    /**
     * @brief Provides const access to the underlying nlohmann::json object.
     */
    const nlohmann::json& raw() const { return m_jsonRef; }

    /**
     * @brief Provides non-const access to the underlying nlohmann::json object.
     * @warning Use with caution. Modifying the raw object directly should trigger notification if appropriate.
     */
    nlohmann::json& raw() {
        // Accessing raw() implies potential modification, so notify.
        if (m_notifier) { 
            m_notifier(); 
        }
        return m_jsonRef; 
    }

    /**
     * @brief Assigns a value to the JSON node this object wraps.
     * @note This operation modifies the underlying JSON and triggers the modification notifier.
     *       Only works if the JSONNode was obtained from a non-const context (notifier is valid).
     */
    template <typename T, typename = std::enable_if_t<!std::is_same_v<std::decay_t<T>, JSONNode>>>
    JSONNode& operator=(const T& value) {
        if (m_notifier) { // Only allow assignment if mutable context (notifier exists)
            try {
                // Check if the value is actually different before notifying
                // This avoids unnecessary version bumps if assigning the same value
                if (m_jsonRef != value) { 
                    m_jsonRef = value; // Use nlohmann::json's assignment
                    m_notifier(); // Notify handler: sets dirty, increments version
                }
            } catch (const nlohmann::json::exception& e) {
                 std::cerr << "JSONNode assignment error: " << e.what() << std::endl;
            }
        } else {
            std::cerr << "Warning: Attempting assignment on a const JSONNode. Operation ignored." << std::endl;
        }
        return *this;
    }
    
    // Overload for const char*
    JSONNode& operator=(const char* value) {
        if (m_notifier) {
            try {
                 if (m_jsonRef != value) { 
                    m_jsonRef = value;
                    m_notifier();
                 }
            } catch (const nlohmann::json::exception& e) {
                 std::cerr << "JSONNode assignment error: " << e.what() << std::endl;
            }
        } else {
             std::cerr << "Warning: Attempting assignment on a const JSONNode. Operation ignored." << std::endl;
        }
        return *this;
    }

private:
    nlohmann::json& m_jsonRef; ///< Reference to the wrapped JSON object (mutable).
    ModificationNotifier m_notifier; ///< Callback function to notify owner of modification.
};

#endif // JSON_NODE_H
